6.11. Проектирование функциональных UI
Проектирование функциональных UI
1. Функциональный UI и визуальный дизайн
Прежде чем перейти к проектированию, необходимо чётко развести две задачи:
- Визуальный дизайн — отвечает на вопрос «как это выглядит?»: композиция, цвет, типографика, анимации, эмоциональное восприятие. Его цель — сделать интерфейс привлекательным, интуитивным и доступным.
- Функциональное проектирование UI — отвечает на вопрос «как это работает?»: какие действия доступны пользователю, в какой последовательности, при каких условиях, как состояние системы отражается в интерфейсе, как обрабатываются ошибки. Его цель — сделать взаимодействие корректным, предсказуемым и безопасным.
Разработчик, проектирующий функциональный UI, работает с моделями состояний, контрактами API, ограничениями домена и сценариями использования. Художник создаёт макет кнопки «Оплатить»; инженер определяет, когда она должна быть активна, что происходит при нажатии, как обрабатывается таймаут, как отображается прогресс, как восстанавливается состояние после обновления страницы.
Если визуальный дизайн нарушается — страдает удобство.
Если функциональное проектирование нарушается — страдает корректность.
Например: кнопка «Оплатить» серая и некрасивая — плохо. Кнопка «Оплатить» активна при нулевой сумме — критическая ошибка.
2. UI как отражение бизнес-состояния
Фундаментальный принцип: пользовательский интерфейс не управляет системой — он отражает её состояние и предоставляет канал для воздействия.
Это означает:
- Любое действие пользователя — это запрос на изменение состояния, а не прямая команда.
- Любое отображаемое значение — это проекция текущего состояния, а не кэшированная копия.
- Любое ограничение интерфейса (недоступная кнопка, скрытое поле) — это выражение бизнес-правила, а не произвольное решение дизайнера.
Пример:
В системе управления заказами статус «Оплачен» означает:
- Нельзя изменить состав заказа,
- Можно инициировать отгрузку,
- Нельзя отменить без возврата средств.
Функциональный UI должен:
- Отключать элементы редактирования при статусе «Оплачен»,
- Делать доступной кнопку «Отгрузить»,
- Скрывать или переключать кнопку «Отменить» на «Запросить возврат».
Если эти правила реализованы только на фронтенде, возможна рассогласованность: пользователь может отправить запрос на изменение состава через Postman, и бэкенд его примет (если там нет проверки).
Если реализованы только на бэкенде — UI будет выглядеть «мертвым»: кнопки активны, но при нажатии возвращается ошибка 403.
Правильный подход:
- Бизнес-правила декларируются в домене (например,
Order.CanModify()→bool). - Бэкенд предоставляет метаданные о доступных действиях (например,
GET /orders/123возвращает данные иavailableActions: ["ship", "requestRefund"]). - Фронтенд строит UI на основе этих метаданных, а не на основе локальной логики.
Такой подход называется HATEOAS (Hypermedia as the Engine of Application State) — один из принципов REST, редко реализуемый полностью, но крайне полезный как идеал.
3. Алгоритм проектирования функционального UI
Процесс начинается с анализа сценариев взаимодействия.
Шаг 1. Выявление акторов и целей
Кто использует интерфейс и зачем?
- Администратор хочет массово изменить статусы заказов.
- Покупатель хочет проследить этапы доставки.
- Оператор колл-центра хочет быстро найти заказ по телефону и продиктовать статус.
У каждого — разные цели, следовательно, разные приоритеты в интерфейсе:
- Для администратора — таблица с чекбоксами и bulk-действиями,
- Для покупателя — линейный прогресс-бар с пояснениями,
- Для оператора — поле поиска с автодополнением и крупным отображением статуса.
Проектирование начинается с пользовательских историй, но как источника для выявления ограничений и инвариантов.
Шаг 2. Моделирование состояний и переходов
Каждый UI-экран — это проекция состояния агрегата или процесса. Необходимо явно описать:
- Какие состояния возможны (например,
Draft → Confirmed → Paid → Shipped → Delivered), - Какие переходы разрешены,
- Какие условия необходимы для перехода (например, «только после оплаты можно перейти в Shipped»),
- Какие побочные эффекты сопровождают переход (email, webhook, изменение складских остатков).
Это часто оформляется как диаграмма состояний (state machine) для согласования между разработчиками, тестировщиками и аналитиками.
Если переход не моделируется явно, он реализуется императивно в коде (if (order.Status == "Paid") ...), что ведёт к:
- Рассогласованности между UI и бэкендом,
- Ошибкам при параллельных изменениях,
- Сложности добавления новых состояний.
Шаг 3. Определение контракта данных
UI требует структурированный контекст:
- Текущее состояние объекта,
- Возможные действия,
- Ограничения ввода (валидация в реальном времени),
- Справочники и enum’ы (со значениями и локализацией),
- История изменений (если нужна аудиторская трассировка).
Контракт должен быть самодостаточным. Пример плохого API:
{ "status": "paid", "canShip": true }
— откуда UI знает, что canShip означает? Что будет, если завтра добавится canCancel?
Хороший API:
{
"status": "paid",
"transitions": [
{ "action": "ship", "label": "Отгрузить", "requires": ["warehouseNote"] },
{ "action": "requestRefund", "label": "Запросить возврат" }
],
"validationRules": {
"warehouseNote": { "required": true, "maxLength": 255 }
}
}
Такой подход:
- Снижает связность UI и бэкенда,
- Позволяет динамически менять логику без деплоя фронтенда,
- Упрощает локализацию и A/B-тестирование интерфейсов.
Шаг 4. Управление асинхронностью и частичными состояниями
В распределённых системах UI часто работает с неполным или устаревшим состоянием. Проектирование должно учитывать:
- Что показывать во время выполнения операции? (индикатор, блокировка кнопки),
- Что делать при таймауте? (повтор, отмена, сохранение черновика),
- Как синхронизировать состояние после переподключения? (оптимистичные обновления, операционные трансформации),
- Как обрабатывать конфликты? («другой пользователь изменил этот заказ»).
Пример: редактирование заказа.
- Пользователь меняет адрес,
- В это время оператор отменяет заказ,
- Пользователь нажимает «Сохранить».
Если UI не проверяет условие If-Match (ETag) или не получает события OrderCancelled, сохранение пройдёт — и отменённый заказ снова станет активным.
Решения:
- Использовать оптимистичную блокировку (ETag в заголовках),
- Подписываться на события домена через SSE или WebSocket,
- Вводить локальные сайд-эффекты (например, отмена сохранения при получении события
OrderCancelled).
Шаг 5. Валидация
Валидация в UI — ускорение проверок на бэкенде.
| Уровень | Цель | Пример |
|---|---|---|
| UI-валидация (мгновенная) | Улучшить UX, снизить нагрузку | Проверка формата email при потере фокуса (/^[^@]+@[^@]+\.[^@]+$/) |
| Фронтенд-валидация (перед отправкой) | Предотвратить заведомо невалидные запросы | Проверка общей суммы ≥ минимальной корзины |
| Бэкенд-валидация (на входе) | Гарантия целостности | Повторная проверка формата, бизнес-правил, доступа |
| Доменная валидация (в сущностях) | Защита инвариантов | Order.AddLine() проверяет, что товар активен и в наличии |
Важно: никакая валидация на фронтенде не отменяет проверки на бэкенде. UI можно обойти; бэкенд — нет.
Шаг 6. Отображение ошибок
Ошибка — часть взаимодействия. UX ошибки должен быть:
- Конкретным: «Срок действия карты истёк» вместо «Ошибка оплаты»,
- Действенным: предложить решение — «Введите новую карту» или «Свяжитесь с банком»,
- Локализованным: код ошибки (
CARD_EXPIRED) + сообщение на языке пользователя, - Логируемым: уникальный correlation ID для диагностики.
Идеальный контракт ошибки:
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "На счёте недостаточно средств",
"details": {
"balance": 1200,
"required": 1500,
"currency": "RUB"
},
"resolution": {
"actions": ["topUp", "useBonus"],
"links": {
"topUp": "/wallet/top-up",
"bonus": "/profile/bonus"
}
}
}
}
4. Разделение ответственности между UI и бизнес-логикой
Последствия дублирования логики:
- UI содержит
if (total < 1000) shipping = 300 else shipping = 0, - Бэкенд содержит ту же логику,
- Правило меняется: доставка бесплатна от 800 ₽,
- Забыли обновить фронтенд — пользователь видит 300 ₽, но платит 0,
- Пользователь путается, поддержка получает жалобы.
Как избежать:
- Вынос правил в shared-библиотеку (если стек позволяет — например, TypeScript + .NET через генерацию DTO и валидаторов),
- Предоставление правил через API (например,
GET /pricing/rulesвозвращает{ "freeShippingThreshold": 800 }), - Использование декларативных форм (JSON Schema, form.io), где схема генерируется на бэкенде и интерпретируется на фронтенде.
Последний подход особенно эффективен для внутренних систем: схема формы — это проекция бизнес-модели, а не отдельный артефакт.
5. Доступность и инклюзивность как требования проектирования
Функциональное проектирование UI включает обеспечение доступности (accessibility) — как функциональное требование.
Примеры:
- Кнопка без
aria-label— недоступна для 1% пользователей, что нарушает функциональность. - Цветовая индикация статуса (красный = ошибка) без текста — некорректное отображение состояния.
- Отсутствие
tabindex— блокировка работы через клавиатуру.
Стандарты (WCAG 2.1, Section 508) содержат конкретные критерии, которые можно тестировать автоматически (axe, Lighthouse) и вручную. Их соблюдение — часть контракта UI.